#include <stdlib.h>
#include <unistd.h>
#include <strings.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <resolv.h>
#include <arpa/inet.h>
#include <errno.h>
#include <stdlib.h>

#include "queue.h"

#define DEFAULT_PORT                    6325
#define MAXBUF                                  1024
#define NUM_OF_HEAPS                    4
#define MAX_M_VALUE                             1500
#define MAX_NO_OF_PLAYERS               9
#define CLIENT_WIN                              'C'
#define OTHER_WIN                               'O'
#define WATCHER_WIN                             'K'
#define CLIENT_WIN_AND_TURN                                             'S'
#define OTHER_WIN_AND_TURN                                  'G'
#define INVALID_DESTINATION                                     'D'
#define ILLEGAL_MOVE                                    'I'
#define LEGAL_MOVE                              'L'
#define WATCH_MODE                              'W'
#define CLIENT_TURN                             'T'
#define NOT_CLIENT_TURN                         'N'
#define QUIT_MOVE                               'Q'
#define PROMOTION                               'Z'
#define TOO_MANY_PLAYERS                                                'A'
#define IS_PLAYER                               1
#define IS_WATCHER                              0
#define OUTPUT_BUFFER_SIZE              32
#define CLIENT_MSG_LEN                  MAXBUF - 6
#define SET_PLAYER_TURN(BUFFER, x)      *((int *) (BUFFER + 1 + (4 * sizeof(int)))) = x

int p;
int fdmax;
int isMisere;
int firstPlayer;
int lastPlayerFD;
fd_set master_fds;
int currNumOfPlayers;
int clientIDs[MAX_NO_OF_PLAYERS];
int playerIDs[MAX_NO_OF_PLAYERS];

typedef struct clientMsg {
	int dest;
	char *msg;
} clientMsg;

void initClientMsg(clientMsg *msg) {
	msg->dest = -2;
	msg->msg = NULL;
}

void closeAllResources() {
	int i;
	for (i = 0; i < fdmax + 1; ++i) {
		if (FD_ISSET(i, &master_fds)) {
			close(i);
		}
	}
}

void fillOutputBufferWithHeap(char *outputBuffer, int *heapsCount) {
	int i;
	// Fill the end of the output buffer with the heaps
	// state
	for (i = 0; i < NUM_OF_HEAPS; ++i) {
		// Ugly pointers hack.
		*((int *) (&outputBuffer[1 + (i * sizeof(int))])) = heapsCount[i];
	}
}

void serverTurn(int *heapsCount) {
	//plays the server turn
	int currMaxHeap = NUM_OF_HEAPS - 1;
	int i;
	for (i = NUM_OF_HEAPS - 2; i >= 0; --i) {
		if (heapsCount[i] > heapsCount[currMaxHeap]) {
			currMaxHeap = i;
		}
	}
	--heapsCount[currMaxHeap];
}

int isWatcher(int fd) {
	;
	int i;
	for (i = 0; i < MAX_NO_OF_PLAYERS; ++i) {
		if (playerIDs[i] == fd) {
			return 0;
		}
	}
	return 1;
}

int checkIfGameEnded(char *outputBuffer, int *heapsCount) {
	int i, hasEnded = 1;
	for (i = 0; i < NUM_OF_HEAPS; ++i) {
		if (!heapsCount[i] == 0) {
			hasEnded = 0;
			break;
		}
	}
	if (hasEnded) {
		return 1;
	}
	return 0;
}

int checkArgument(char **argv, int argIndex, int min, int max) {
	char *s;
	int convertedArg;
	convertedArg = strtol(argv[argIndex], &s, 10);

	// Check if the argument is of valid type
	if (strlen(s) != 0) {
		printf("Argument #%d is an invalid argument!\n", argIndex);
		exit(-1);
	}

	// Check if the range of the argument is valid
	if (convertedArg < min || convertedArg > max) {
		printf("Argument #%d must be between %d and %d", argIndex, min, max);
		exit(-1);
	}
	return convertedArg;
}

int generatePayerID(int *playerIDs, int clientFD) {
	int i;
	for (i = 0; i < sizeof(playerIDs); ++i) {
		if (playerIDs[i] == 0) {
			playerIDs[i] = clientFD;
			return i + 1;
		}
	}
	return -1;
}

int recvAndVerify(int fd, char *inputBuffer) {
	int i;
	if ((i = recv(fd, inputBuffer, MAXBUF, 0)) <= 0) {
		if (i == 0) {
			return 0;
		} else {
			closeAllResources();
			perror("recv");
			exit(errno);
		}
	}
	return 1;
}

int getNextTurnFD(queue *playersTurn) {
	int nextID = peek(playersTurn);
	return playerIDs[nextID - 1];
}

void promoteWatcher(int *fd, queue *playersQueue, queue *watchersQueue) {
	int i, j;
	int id = 0;
	int currId;
	int watcherID;
	int playedLastTurn = 0;
	char promotionMessage[1];

	promotionMessage[0] = PROMOTION;
	--currNumOfPlayers;
	// remove from the players list
	for (i = 0; i < MAX_NO_OF_PLAYERS; ++i) {
		if (playerIDs[i] == *fd) {
			playerIDs[i] = 0;
			id = i + 1;
			break;
		}
	}
	// remove from clients list
	for (i = 0; i < MAX_NO_OF_PLAYERS; ++i) {
		if (clientIDs[i] == *fd) {
			clientIDs[i] = 0;
			break;
		}
	}

	// remove from the players turn queue
	if (id) {
		if (playersQueue->q[playersQueue->last] == id) {
			playedLastTurn = 1;
		}

		i = 0;
		while ((currId = dequeue(playersQueue)) != id) {
			++i;
			enqueue(playersQueue, currId);
		}
		i = playersQueue->count - i;
		for (; i > 0; --i) {
			enqueue(playersQueue, dequeue(playersQueue));
		}
		// promote a watcher
		if (watchersQueue->count > 0) {
			++currNumOfPlayers;
			watcherID = dequeue(watchersQueue);
			playerIDs[watcherID - 1] = clientIDs[watcherID - 1];

			if (playedLastTurn) {
				enqueue(playersQueue, watcherID);
			} else {
				for (j = playersQueue->count; j > 1; --j) {
					enqueue(playersQueue, dequeue(playersQueue));
				}
				enqueue(playersQueue, watcherID);
				enqueue(playersQueue, dequeue(playersQueue));
			}

			// Send the promotion message
			send(clientIDs[watcherID - 1], promotionMessage, 1, 0);
			// TODO check faliure
		}

	}

}

int validateMSG(char *buffer, clientMsg *msg) {
	int clientNo = 0;

	if (sscanf(buffer, "MSG %d ", &clientNo) < 1) {
		return 0;
	}
	msg->dest = clientNo;
	msg->msg = buffer;
	return 1;
}

int handleExistingConnection(char *outputBuffer, int *fd, char *inputBuffer,
		int *heapsCount, int *listener, int *id2fd, queue *playersQueue,
		queue *watchersQueue) {

	int j, currTakenTokens, nextTurnID, isLegal = 0, wasPlayerTurn = 0,
			gameEnded = 0, clientHasQuit = 0;
	clientMsg msg;

	if (firstPlayer == 1) {
		firstPlayer = -1;
		enqueue(playersQueue, dequeue(playersQueue));
	}

	//Check if its players turn
	if ((firstPlayer == 1 && getNextTurnFD(playersQueue) == *fd)
			|| (firstPlayer == -1
					&& playerIDs[playersQueue->q[playersQueue->last] - 1] == *fd)) {
		wasPlayerTurn = 1;
	}

	// Handle a client exit
	if (!recvAndVerify(*fd, inputBuffer) || inputBuffer[0] == QUIT_MOVE) {

		memset(inputBuffer, 0, MAXBUF);

		clientHasQuit = 1;

		FD_CLR(*fd, &master_fds);

		// check if he was a player, and if so - promote a
		// watcher (if exists) to a player
		promoteWatcher(fd, playersQueue, watchersQueue);

		// It wasn't the player's turn so we don't need
		// to handle the next turn for the other players
		if (!wasPlayerTurn) {
			return 0;
		}
	}

	// Handle message protocol
	if (validateMSG(inputBuffer, &msg)) {
		int i, clientID;
		// check if the destination is valid
		if (msg.dest == -1 || clientIDs[msg.dest - 1] != 0) {
			// get the sender ID
			for (i = 0; i < MAX_NO_OF_PLAYERS; ++i) {
				if (clientIDs[i] == *fd) {
					clientID = i + 1;
					msg.msg[4] = '0' + clientID;
					break;
				}
			}
			if (msg.dest == -1) {
				// broadcast message
				for (i = 0; i < MAX_NO_OF_PLAYERS; ++i) {
					if (clientIDs[i] != 0 && clientIDs[i] != *fd) {
						if (send(clientIDs[i], msg.msg, MAXBUF, 0) < 0) {
							closeAllResources();
						}
					}
				}
			} else {

				if (send(clientIDs[msg.dest - 1], msg.msg, MAXBUF, 0) < 0) {
					closeAllResources();
				}
			}
		} else {
			outputBuffer[0] = INVALID_DESTINATION;
			if (send(*fd, outputBuffer, OUTPUT_BUFFER_SIZE, 0) < 0) {
				closeAllResources();
			}

		}
		return 0;
	}
	if (currNumOfPlayers == 0) {
		return 0;
	}
	// Handle game protocol
	if (wasPlayerTurn) {
		if (inputBuffer[0] >= 'A' && inputBuffer[0] <= 'D') {
			currTakenTokens = *((int *) (inputBuffer + 1));

			if (currTakenTokens > heapsCount[inputBuffer[0] - 'A']
					|| currTakenTokens < 1) {
				isLegal = ILLEGAL_MOVE;
			} else {
				heapsCount[inputBuffer[0] - 'A'] -= currTakenTokens;
				isLegal = LEGAL_MOVE;
			}

			fillOutputBufferWithHeap(outputBuffer, heapsCount);
			// If the client hasn't quit than he sent an
			// illegal move
		} else if (!clientHasQuit) {
			outputBuffer[0] = ILLEGAL_MOVE;
			if (send(*fd, outputBuffer, OUTPUT_BUFFER_SIZE, 0) < 0) {
				closeAllResources();
			}
			return 0;
		}
		// Client sent a message out of his turn
	} else if (!clientHasQuit) {
		outputBuffer[0] = NOT_CLIENT_TURN;
		if (send(*fd, outputBuffer, OUTPUT_BUFFER_SIZE, 0) < 0) {
			closeAllResources();
		}
		return 0;
	}

	// Check if end of game
	if (isLegal == LEGAL_MOVE) {
		gameEnded = checkIfGameEnded(outputBuffer, heapsCount);
	}

	// Get the ID of the next player in line
	nextTurnID = dequeue(playersQueue);
	enqueue(playersQueue, nextTurnID);

	// If the player quit, notify only the next player
	if (clientHasQuit) {
		for (j = 0; j < fdmax + 1; ++j) {
			if (j == id2fd[nextTurnID]) {
				SET_PLAYER_TURN(outputBuffer, 1);
				outputBuffer[0] = QUIT_MOVE;
				if (send(j, outputBuffer, OUTPUT_BUFFER_SIZE, 0) < 0) {
					closeAllResources();
				}
				return 0;
			}
		}
	}
	// Notify the other players
	for (j = 0; j < fdmax + 1; ++j) {

		if (j != *listener && FD_ISSET(j, &master_fds)) {

			//check if game ended
			if (!gameEnded) {
				if (id2fd[nextTurnID] == j) {
					SET_PLAYER_TURN(outputBuffer, 1);
					outputBuffer[0] = CLIENT_TURN;
				} else {
					SET_PLAYER_TURN(outputBuffer, 0);
					outputBuffer[0] = WATCH_MODE;
				}
				if (*fd == j) {
					outputBuffer[0] = isLegal;
				}
				//handle end of game
			} else {
				if (*fd == j) {
					if (isMisere) {
						outputBuffer[0] = OTHER_WIN_AND_TURN;
					} else {
						outputBuffer[0] = CLIENT_WIN_AND_TURN;
					}
				} else {
					//watcher
					if (isWatcher(j)) {
						outputBuffer[0] = WATCHER_WIN;
						//player
					} else {
						if (isMisere) {
							outputBuffer[0] = CLIENT_WIN;
						} else {
							outputBuffer[0] = OTHER_WIN;
						}
					}
				}

			}

			// Send the message
			if (send(j, outputBuffer, OUTPUT_BUFFER_SIZE, 0) < 0) {
				closeAllResources();
			}
		}
	}
	return gameEnded;
}

void handleNewConnection(int *listener, char *outputBuffer, int *heapsCount,
		queue *playersQueue, queue *watchersQueue, int *id2fd) {

	int clientfd;
	int newID;
	int i;
	struct sockaddr_in client_addr;
	int totalClients = 0;
	socklen_t addrlen = sizeof(client_addr);

	if ((clientfd = accept(*listener, (struct sockaddr*) &client_addr, &addrlen))
			< 0) {
		close(*listener);
		perror("socket--accept");
		exit(errno);
	}

	for (i = 0; i < MAX_NO_OF_PLAYERS; ++i) {
		if (clientIDs[i] != 0) {
			++totalClients;
		}
	}

	if (totalClients == MAX_NO_OF_PLAYERS) {
		outputBuffer[0] = TOO_MANY_PLAYERS;
		send(clientfd, outputBuffer, OUTPUT_BUFFER_SIZE, 0);
		return;
	}

	// Insert the new client to the master set
	FD_SET(clientfd, &master_fds);
	if (clientfd > fdmax) {
		fdmax = clientfd;
	}

	lastPlayerFD = clientfd;
	// Generate a new ID to the client
	newID = generatePayerID(clientIDs, clientfd);
	id2fd[newID] = clientfd;

	// Fill the start of the output buffer with the isMisere flag.
	outputBuffer[0] = (char) ('0' + isMisere);
	fillOutputBufferWithHeap(outputBuffer, heapsCount);

	// Fill in the p and ID values to the buffer
	*((int *) (&outputBuffer[1 + (5 * sizeof(int))])) = p;
	*((int *) (&outputBuffer[1 + (6 * sizeof(int))])) = newID;

	// Signal to the client if it will be a player or a watcher
	if (currNumOfPlayers < p) {
		// This one is a player, put him in the players queue

		++currNumOfPlayers;

		playerIDs[newID - 1] = clientfd;

		*((int *) (&outputBuffer[1 + (7 * sizeof(int))])) =
		IS_PLAYER;

		if (firstPlayer == -1) {
			for (i = playersQueue->count; i > 1; --i) {
				enqueue(playersQueue, dequeue(playersQueue));
			}
			enqueue(playersQueue, newID);
			enqueue(playersQueue, dequeue(playersQueue));
		} else {
			enqueue(playersQueue, newID);
		}
		// Check if its the first player
		// If so, it's his turn now, else, his not playing now
		if (firstPlayer == 0 || playersQueue->count == 1) {
			firstPlayer = 1;
			*((int *) (&outputBuffer[1 + (4 * sizeof(int))])) = 1;
		} else {
			*((int *) (&outputBuffer[1 + (4 * sizeof(int))])) = 0;
		}

	} else {
		// this one is a watcher. put him him in the watchers queue
		*((int *) (&outputBuffer[1 + (7 * sizeof(int))])) =
		IS_WATCHER;
		*((int *) (&outputBuffer[1 + (4 * sizeof(int))])) = 0;
		enqueue(watchersQueue, newID);
	}

	// Send the message to the new client
	if ((send(clientfd, outputBuffer, OUTPUT_BUFFER_SIZE, 0)) < 0) {
		closeAllResources();
	}
}

int main(int argc, char **argv) {

	int i;
	int M;
	int port;

	int listener;
	fd_set read_fds;

	int pendingClients;
	queue playersQueue;
	queue watchersQueue;
	struct sockaddr_in self;
	char inputBuffer[MAXBUF];
	int heapsCount[NUM_OF_HEAPS];

	int id2fd[MAX_NO_OF_PLAYERS];
	char outputBuffer[OUTPUT_BUFFER_SIZE];

	// Check number of arguments
	if (argc < 4 || argc > 5) {
		printf("Invalid number of arguments.\n");
		printf("Usage: nim-server p M isMisere [port]\n");
		exit(1);
	}

	// Validate and assign arguments.
	p = checkArgument(argv, 1, 2, 9);
	M = checkArgument(argv, 2, 1, MAX_M_VALUE);
	isMisere = checkArgument(argv, 3, 0, 1);
	if (argc > 4) {
		port = checkArgument(argv, 4, 1024, 65535);
	} else {
		port = DEFAULT_PORT;
	}

	// Initialize heaps
	for (i = 0; i < NUM_OF_HEAPS; ++i) {
		heapsCount[i] = M;
	}

	// Create socket
	if ((listener = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
		perror("Socket");
		exit(errno);
	}

	// Initialize values for the address and port
	bzero(&self, sizeof(self));
	self.sin_family = AF_INET;
	self.sin_port = htons(port);
	self.sin_addr.s_addr = INADDR_ANY;

	// bind the address and port to the socket
	if (bind(listener, (struct sockaddr*) &self, sizeof(self)) != 0) {
		close(listener);
		perror("socket--bind");
		exit(errno);
	}

	// Make the socket listen
	if (listen(listener, 20) != 0) {
		close(listener);
		perror("socket--listen");
		exit(errno);
	}

	// Initialize FD sets
	FD_ZERO(&read_fds);
	FD_ZERO(&master_fds);

	// Initialize players IDs
	memset(clientIDs, 0, sizeof(clientIDs));

	// Initialize firstPlayer
	firstPlayer = 0;

	// Initialize queues
	init_queue(&playersQueue);
	init_queue(&watchersQueue);

	currNumOfPlayers = 0;

	// Add the listener to the master FD set
	FD_SET(listener, &master_fds);
	fdmax = listener;

	// Let the games begin!
	while (1) {

		// Copy the master to the read group
		read_fds = master_fds;

		if ((pendingClients = select(fdmax + 1, &read_fds, NULL, NULL, NULL))
				== -1) {
			perror("select");
			exit(errno);
		}

		for (i = 0; i < fdmax + 1 && pendingClients > 0; ++i) {

			if (FD_ISSET(i, &read_fds)) {
				--pendingClients;

				if (i == listener) {
					handleNewConnection(&listener, outputBuffer, heapsCount,
							&playersQueue, &watchersQueue, id2fd);
				} else {
					handleExistingConnection(outputBuffer, &i, inputBuffer,
							heapsCount, &listener, id2fd, &playersQueue,
							&watchersQueue);
				}
			}
		}
	}
	closeAllResources();

	return 0;
}
